Design LinkedIn

Last Updated: December 19, 2025

Ashish

Ashish Pratap Singh

hard

In this chapter, we will explore the low-level design of LinkedIn like system in detail.

Let's start by clarifying the requirements:

1. Clarifying Requirements

Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.

Here is an example of how a conversation between the candidate and the interviewer might unfold:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • Profile Management: Allow members to create and update their professional profiles
  • Connection Management: Members can send connection requests to other members. Requests can be accepted or rejected.
  • Posting: Members can create text-based posts.
  • News Feed: Members can view a feed of posts made by their connections.
  • Post Interactions: Members can like and comment on posts.
  • Notifications: Members receive notifications for: Connection requests, Likes/Comments on their posts
  • Search: Members can search for other members by name.

1.2 Non-Functional Requirements

  • Modularity: The system should be organized into well separated components
  • Extensibility: The design should be extensible to support future features like job postings, or premium accounts
  • Maintainability: Code should follow object-oriented design principles and be easy to test, debug, and extend

2. Identifying Core Entities

Core entities are the foundational building blocks of our system. We identify them by analyzing key nouns (e.g., user, profile, connection request, message, activity log) and actions (e.g., connect, search, update, message, log) from the functional requirements. These typically translate into classes, enums, or interfaces in an object-oriented design.

Let’s walk through the key features and extract the relevant entities.

1. Members need profiles with experience and education.

This directly points to a Member as the central entity representing a user. Each Member has a Profile, which in turn is composed of smaller, distinct entities: Experience and Education. These classes encapsulate the professional and academic history of a member.

2. Members can connect with each other via requests.

The relationship between two members is a key concept. This leads to a Connection entity, which models the request itself, including the sender, receiver, and its current state. To manage these states consistently, a ConnectionStatus enum (PENDING, ACCEPTED, etc.) is required. The logic for managing these connections is encapsulated in a ConnectionService.

3. Members can create posts, which appear in their connections' news feeds.

This introduces the Post entity as a piece of user-generated content. A NewsFeed entity is needed to represent the collection of posts displayed to a member. The logic for generating this feed is handled by a NewsFeedService. To allow for different ways of organizing the feed, a FeedSortingStrategy interface is introduced.

4. Members can interact with posts via likes and comments.

Interactions on a Post are modeled as their own entities. This leads to Like and Comment classes, which are associated with a Post and capture who performed the action and when.

5. Members receive notifications for key events.

To handle alerts, a Notification entity is needed to encapsulate the message content and its type. A NotificationType enum (CONNECTION_REQUEST, POST_LIKE, etc.) standardizes the kinds of alerts. A NotificationService is responsible for dispatching these notifications.

6. Members can search for other members by name.

This functionality is encapsulated within a SearchService, which operates on the collection of Member entities to find matches.

7. The system needs a simple, unified entry point.

To manage the complexity of interactions between various services and data models, a LinkedInSystem class is introduced. It acts as a Facade and Singleton, providing a high-level API for all client operations.

These core entities define the essential abstractions of the LinkedIn system and will guide the structure of your low-level design and class diagrams.

3. Class Design

3.1 Class Definitions

The system is composed of several types of classes, each with a distinct role.

Enums

Enums
  • ConnectionStatus: Defines the lifecycle states of a connection request (PENDING, ACCEPTED, REJECTED).
  • NotificationType: Classifies the type of notification being sent (CONNECTION_REQUEST, POST_LIKE).

Data Classes

Education, Experience

Simple data classes that capture a member's academic and professional history.

Education
Experience

Profile

A container class that aggregates a member's summary, experiences, and educations.

Profile

Connection

Represents a connection request between two members, tracking its status and timestamps.

Connection

Like, Comment

Data classes that model user interactions on a Post.

Like
Comment

Notification

A data class representing a single notification message to be sent to a user.

Core Classes

Member

Represents a user of the platform.

Memb

It is a central class that acts as a concrete Observer (implementing NotificationObserver) to receive notifications. Its complex creation is handled by a nested Builder. It holds its Profile, a set of connections, and a list of notifications.

Post

Represents a single post in the news feed.

It is a concrete Subject (extending Subject) that notifies its author when other members interact with it (by liking or commenting).

NewsFeed

Responsible for displaying a collection of posts according to a given FeedSortingStrategy.

ConnectionService, NewsFeedService, NotificationService, SearchService

These are service-layer classes that encapsulate specific business logic. For instance, ConnectionService handles sending and accepting requests, while NewsFeedService manages creating posts and generating feeds for members.

LinkedInSystem (Singleton & Facade)

The primary entry point for the entire application.

LinkedInSystem

It orchestrates all the services and manages the main data stores (e.g., the map of all members). It provides a simplified interface to the client, hiding the complex interactions between the various components.

3.2 Class Relationships

The relationships between classes define the system's structure and data flow.

Composition

  • A Member "has-a" Profile. The Profile's lifecycle is managed by the Member.
  • A Profile "has-a" list of Experience and Education objects.
  • LinkedInSystem "has-a" set of core services (ConnectionService, NewsFeedService, etc.) and manages the collection of all Members.

Association

  • A Member is associated with a set of other Members as connections.
  • A Connection object links two Members.
  • A Post is associated with its author Member. Likes and Comments are also associated with the Member who created them.
  • A NewsFeed is associated with a FeedSortingStrategy to determine the display order.
  • A Post (Subject) is associated with its NotificationObservers (typically the author Member).

Inheritance

  • Member implements the NotificationObserver interface.
  • Post extends the abstract Subject class.
  • ChronologicalSortStrategy implements the FeedSortingStrategy interface.

Dependency

  • LinkedInSystem (Facade) depends on its various service classes to execute user commands.
  • ConnectionService depends on NotificationService to inform users of new requests.
  • A client depends on Member.Builder to construct new Member objects.
  • Post depends on Notification to create update messages for its observers.

3.3 Key Design Patterns

Strategy Pattern

FeedSortingStrategy

The FeedSortingStrategy allows the algorithm for sorting the news feed to be encapsulated and made interchangeable. The system can easily support new sorting methods (e.g., by relevance, by engagement) by creating new strategy classes without altering the NewsFeed or NewsFeedService.

Observer Pattern

NotificationObserver

This pattern is fundamental for real-time notifications. A Post (Subject) notifies its author Member (Observer) whenever a new like or comment is added. This decouples the action (e.g., liking a post) from the notification logic.

Builder Pattern

The Member.Builder class is used for the step-by-step construction of a Member object. This pattern is ideal for objects with many optional parameters (like summary, experience, education), providing a fluent and readable API while ensuring the Member object is created in a valid state.

Facade Pattern

The LinkedInSystem class serves as a facade. It provides a simple, high-level API (sendConnectionRequest, createPost, viewNewsFeed) that hides the complex internal workflows involving multiple services, data models, and notifications.

Singleton Pattern

LinkedInSystem is implemented as a singleton to ensure a single, globally accessible point of control for the entire application. This prevents state inconsistencies and centralizes the management of services and data.

3.4 Full Class Diagram

LinkedIn Class Diagram

4. Implementation

4.1 Enums

Defines standard statuses for connection requests and notification types to maintain consistency across services.

1class ConnectionStatus(Enum):
2    PENDING = "PENDING"
3    ACCEPTED = "ACCEPTED"
4    REJECTED = "REJECTED"
5    WITHDRAWN = "WITHDRAWN"
6
7
8class NotificationType(Enum):
9    CONNECTION_REQUEST = "CONNECTION_REQUEST"
10    POST_LIKE = "POST_LIKE"
11    POST_COMMENT = "POST_COMMENT"

4.2 Profile Components: Education, Experience, Profile

Captures a member’s professional background. Supports dynamic updates and display functionality.

1class Education:
2    def __init__(self, school: str, degree: str, start_year: int, end_year: int):
3        self.school = school
4        self.degree = degree
5        self.start_year = start_year
6        self.end_year = end_year
7
8    def __str__(self) -> str:
9        return f"{self.degree}, {self.school} ({self.start_year} - {self.end_year})"
10
11
12class Experience:
13    def __init__(self, title: str, company: str, start_date: date, end_date: Optional[date]):
14        self.title = title
15        self.company = company
16        self.start_date = start_date
17        self.end_date = end_date
18
19    def __str__(self) -> str:
20        end_str = "Present" if self.end_date is None else str(self.end_date)
21        return f"{self.title} at {self.company} ({self.start_date} to {end_str})"
22
23
24class Profile:
25    def __init__(self):
26        self.summary: Optional[str] = None
27        self.experiences: List[Experience] = []
28        self.educations: List[Education] = []
29
30    def set_summary(self, summary: str) -> None:
31        self.summary = summary
32
33    def add_experience(self, experience: Experience) -> None:
34        self.experiences.append(experience)
35
36    def add_education(self, education: Education) -> None:
37        self.educations.append(education)
38
39    def display(self) -> None:
40        print(f"  Summary: {self.summary if self.summary else 'N/A'}")
41
42        print("  Experience:")
43        if not self.experiences:
44            print("    - None")
45        else:
46            for exp in self.experiences:
47                print(f"    - {exp}")
48
49        print("  Education:")
50        if not self.educations:
51            print("    - None")
52        else:
53            for edu in self.educations:
54                print(f"    - {edu}")

4.3 Member (Builder + Observer Pattern)

Represents a LinkedIn member. Supports profile creation via the Builder Pattern and acts as an observer for real-time notifications.

1class Member(NotificationObserver):
2    def __init__(self, member_id: str, name: str, email: str, profile: Profile):
3        self.id = member_id
4        self.name = name
5        self.email = email
6        self.profile = profile
7        self.connections: Set['Member'] = set()
8        self.notifications: List['Notification'] = []
9
10    def get_id(self) -> str:
11        return self.id
12
13    def get_name(self) -> str:
14        return self.name
15
16    def get_email(self) -> str:
17        return self.email
18
19    def get_connections(self) -> Set['Member']:
20        return self.connections
21
22    def get_profile(self) -> Profile:
23        return self.profile
24
25    def add_connection(self, member: 'Member') -> None:
26        self.connections.add(member)
27
28    def display_profile(self) -> None:
29        print(f"\n--- Profile for {self.name} ({self.email}) ---")
30        self.profile.display()
31        print(f"  Connections: {len(self.connections)}")
32
33    def view_notifications(self) -> None:
34        print(f"\n--- Notifications for {self.name} ---")
35        unread_notifications = [n for n in self.notifications if not n.is_read()]
36        
37        if not unread_notifications:
38            print("  No new notifications.")
39            return
40
41        for notification in unread_notifications:
42            print(f"  - {notification.get_content()}")
43            notification.mark_as_read()
44
45    def update(self, notification: 'Notification') -> None:
46        self.notifications.append(notification)
47        print(f"Notification pushed to {self.name}: {notification.get_content()}")
48
49    class Builder:
50        def __init__(self, name: str, email: str):
51            self.id = str(uuid.uuid4())
52            self.name = name
53            self.email = email
54            self.profile = Profile()
55
56        def with_summary(self, summary: str) -> 'Member.Builder':
57            self.profile.set_summary(summary)
58            return self
59
60        def add_experience(self, experience: Experience) -> 'Member.Builder':
61            self.profile.add_experience(experience)
62            return self
63
64        def add_education(self, education: Education) -> 'Member.Builder':
65            self.profile.add_education(education)
66            return self
67
68        def build(self) -> 'Member':
69            return Member(self.id, self.name, self.email, self.profile)

4.4 Connection

Models a connection request between members and tracks its lifecycle state.

1class Connection:
2    def __init__(self, from_member: Member, to_member: Member):
3        self.from_member = from_member
4        self.to_member = to_member
5        self.status = ConnectionStatus.PENDING
6        self.requested_at = datetime.now()
7        self.accepted_at: Optional[datetime] = None
8
9    def get_from_member(self) -> Member:
10        return self.from_member
11
12    def get_to_member(self) -> Member:
13        return self.to_member
14
15    def get_status(self) -> ConnectionStatus:
16        return self.status
17
18    def set_status(self, status: ConnectionStatus) -> None:
19        self.status = status
20        if status == ConnectionStatus.ACCEPTED:
21            self.accepted_at = datetime.now()

4.5 Post, Like, Comment (Observer Pattern)

Each post tracks likes and comments. Uses the Observer Pattern to notify the post’s author of new interactions.

1class Post(Subject):
2    def __init__(self, author: Member, content: str):
3        super().__init__()
4        self.id = str(uuid.uuid4())
5        self.author = author
6        self.content = content
7        self.created_at = datetime.now()
8        self.likes: List[Like] = []
9        self.comments: List[Comment] = []
10        self.add_observer(author)
11
12    def add_like(self, member: Member) -> None:
13        self.likes.append(Like(member))
14        notification_content = f"{member.get_name()} liked your post."
15        notification = Notification(self.author.get_id(), NotificationType.POST_LIKE, notification_content)
16        self.notify_observers(notification)
17
18    def add_comment(self, member: Member, text: str) -> None:
19        self.comments.append(Comment(member, text))
20        notification_content = f"{member.get_name()} commented on your post: \"{text}\""
21        notification = Notification(self.author.get_id(), NotificationType.POST_COMMENT, notification_content)
22        self.notify_observers(notification)
23
24    def get_id(self) -> str:
25        return self.id
26
27    def get_author(self) -> Member:
28        return self.author
29
30    def get_content(self) -> str:
31        return self.content
32
33    def get_created_at(self) -> datetime:
34        return self.created_at
35
36    def get_likes(self) -> List[Like]:
37        return self.likes
38
39    def get_comments(self) -> List[Comment]:
40        return self.comments
41
42class Like:
43    def __init__(self, member: Member):
44        self.member = member
45        self.created_at = datetime.now()
46
47    def get_member(self) -> Member:
48        return self.member
49
50class Comment:
51    def __init__(self, author: Member, text: str):
52        self.author = author
53        self.text = text
54        self.created_at = datetime.now()
55
56    def get_author(self) -> Member:
57        return self.author
58
59    def get_text(self) -> str:
60        return self.text

4.6 NewsFeed (Strategy Pattern)

1class NewsFeed:
2    def __init__(self, posts: List[Post]):
3        self.posts = posts
4
5    def display(self, strategy: FeedSortingStrategy) -> None:
6        sorted_posts = strategy.sort(self.posts)
7        if not sorted_posts:
8            print("  Your news feed is empty.")
9            return
10
11        for post in sorted_posts:
12            print("----------------------------------------")
13            print(f"Post by: {post.get_author().get_name()} (at {post.get_created_at().date()})")
14            print(f"Content: {post.get_content()}")
15            print(f"Likes: {len(post.get_likes())}, Comments: {len(post.get_comments())}")
16            print("----------------------------------------")

Implements the Strategy Pattern to support multiple feed sorting strategies. Currently defaults to reverse chronological.

4.7 Notification

Defines a generic notification system using the Observer Pattern.

1class Notification:
2    def __init__(self, member_id: str, notification_type: NotificationType, content: str):
3        self.id = str(uuid.uuid4())
4        self.member_id = member_id
5        self.type = notification_type
6        self.content = content
7        self.created_at = datetime.now()
8        self._is_read = False
9
10    def get_content(self) -> str:
11        return self.content
12
13    def mark_as_read(self) -> None:
14        self._is_read = True
15
16    def is_read(self) -> bool:
17        return self._is_read

4.8 Observer

1class NotificationObserver(ABC):
2    @abstractmethod
3    def update(self, notification: 'Notification') -> None:
4        pass
5
6class Subject:
7    def __init__(self):
8        self.observers: List[NotificationObserver] = []
9
10    def add_observer(self, observer: NotificationObserver) -> None:
11        self.observers.append(observer)
12
13    def remove_observer(self, observer: NotificationObserver) -> None:
14        if observer in self.observers:
15            self.observers.remove(observer)
16
17    def notify_observers(self, notification: 'Notification') -> None:
18        for observer in self.observers:
19            observer.update(notification)

4.9 FeedSortingStrategy

1class FeedSortingStrategy(ABC):
2    @abstractmethod
3    def sort(self, posts: List[Post]) -> List[Post]:
4        pass
5
6class ChronologicalSortStrategy(FeedSortingStrategy):
7    def sort(self, posts: List[Post]) -> List[Post]:
8        return sorted(posts, key=lambda post: post.get_created_at(), reverse=True)

4.10 NotificationService

1class NotificationService:
2    def send_notification(self, member: Member, notification: Notification) -> None:
3        member.update(notification)

4.11 ConnectionService

1class ConnectionService:
2    def __init__(self, notification_service: NotificationService):
3        self.notification_service = notification_service
4        self.connection_requests: Dict[str, Connection] = {}
5        self.lock = threading.Lock()
6
7    def send_request(self, from_member: Member, to_member: Member) -> str:
8        connection = Connection(from_member, to_member)
9        request_id = str(uuid.uuid4())
10        
11        with self.lock:
12            self.connection_requests[request_id] = connection
13
14        print(f"{from_member.get_name()} sent a connection request to {to_member.get_name()}.")
15
16        notification = Notification(
17            to_member.get_id(),
18            NotificationType.CONNECTION_REQUEST,
19            f"{from_member.get_name()} wants to connect with you. Request ID: {request_id}"
20        )
21        self.notification_service.send_notification(to_member, notification)
22
23        return request_id
24
25    def accept_request(self, request_id: str) -> None:
26        with self.lock:
27            request = self.connection_requests.get(request_id)
28            
29            if request and request.get_status() == ConnectionStatus.PENDING:
30                request.set_status(ConnectionStatus.ACCEPTED)
31
32                from_member = request.get_from_member()
33                to_member = request.get_to_member()
34
35                from_member.add_connection(to_member)
36                to_member.add_connection(from_member)
37
38                print(f"{to_member.get_name()} accepted the connection request from {from_member.get_name()}.")
39                del self.connection_requests[request_id]
40            else:
41                print("Invalid or already handled request ID.")

4.12 NewsFeedService

1class NewsFeedService:
2    def __init__(self):
3        self.all_posts: Dict[str, List[Post]] = defaultdict(list)
4        self.lock = threading.Lock()
5
6    def add_post(self, member: Member, post: Post) -> None:
7        with self.lock:
8            self.all_posts[member.get_id()].append(post)
9
10    def get_member_posts(self, member: Member) -> List[Post]:
11        return self.all_posts.get(member.get_id(), [])
12
13    def display_feed_for_member(self, member: Member, feed_sorting_strategy: FeedSortingStrategy) -> None:
14        feed_posts = []
15        
16        for connection in member.get_connections():
17            connection_posts = self.all_posts.get(connection.get_id(), [])
18            feed_posts.extend(connection_posts)
19
20        news_feed = NewsFeed(feed_posts)
21        news_feed.display(feed_sorting_strategy)

4.13 SearchService

1class SearchService:
2    def __init__(self, members: Collection[Member]):
3        self.members = members
4
5    def search_by_name(self, name: str) -> List[Member]:
6        return [member for member in self.members if name.lower() in member.get_name().lower()]

4.14 LinkedInSystem

This class is a Singleton that provides a simplified, high-level API to the entire complex subsystem.

1class LinkedInSystem:
2    _instance: Optional['LinkedInSystem'] = None
3    _lock = threading.Lock()
4
5    def __new__(cls):
6        if cls._instance is None:
7            with cls._lock:
8                if cls._instance is None:
9                    cls._instance = super().__new__(cls)
10        return cls._instance
11
12    def __init__(self):
13        if hasattr(self, 'initialized'):
14            return
15
16        self.members: Dict[str, Member] = {}
17        self.connection_service = ConnectionService(NotificationService())
18        self.news_feed_service = NewsFeedService()
19        self.search_service = SearchService(self.members.values())
20        self.initialized = True
21
22    @classmethod
23    def get_instance(cls) -> 'LinkedInSystem':
24        return cls()
25
26    def register_member(self, member: Member) -> None:
27        self.members[member.get_id()] = member
28        print(f"New member registered: {member.get_name()}")
29
30    def get_member(self, name: str) -> Optional[Member]:
31        for member in self.members.values():
32            if member.get_name() == name:
33                return member
34        return None
35
36    def send_connection_request(self, from_member: Member, to_member: Member) -> str:
37        return self.connection_service.send_request(from_member, to_member)
38
39    def accept_connection_request(self, request_id: str) -> None:
40        self.connection_service.accept_request(request_id)
41
42    def create_post(self, member_id: str, content: str) -> None:
43        author = self.members[member_id]
44        post = Post(author, content)
45        self.news_feed_service.add_post(author, post)
46        print(f"{author.get_name()} created a new post.")
47
48    def get_latest_post_by_member(self, member_id: str) -> Optional[Post]:
49        member_posts = self.news_feed_service.get_member_posts(self.members[member_id])
50        if not member_posts:
51            return None
52        return member_posts[-1]
53
54    def view_news_feed(self, member_id: str) -> None:
55        member = self.members[member_id]
56        print(f"\n--- News Feed for {member.get_name()} ---")
57        self.news_feed_service.display_feed_for_member(member, ChronologicalSortStrategy())
58
59    def search_member_by_name(self, name: str) -> List[Member]:
60        return self.search_service.search_by_name(name)
  • Facade Pattern: The facade provides simple methods like sendConnectionRequest and viewNewsFeed, hiding the complex interactions between the various services and data models. A client interacts with the system through this single, clean interface.
  • Singleton Pattern: A single LinkedInSystem instance acts as the central point of control and data management for the entire application. Double-checked locking ensures thread-safe lazy initialization.

4.15 LinkedInDemo

The demo class validates the entire system by simulating interactions on the professional network.

1class LinkedInDemo:
2    @staticmethod
3    def main():
4        system = LinkedInSystem.get_instance()
5
6        # 1. Create Members using the Builder Pattern
7        print("--- 1. Member Registration ---")
8        alice = Member.Builder("Alice", "[email protected]") \
9            .with_summary("Senior Software Engineer with 10 years of experience.") \
10            .add_experience(Experience("Sr. Software Engineer", "Google", date(2018, 1, 1), None)) \
11            .add_experience(Experience("Software Engineer", "Microsoft", date(2014, 6, 1), date(2017, 12, 31))) \
12            .add_education(Education("Princeton University", "M.S. in Computer Science", 2012, 2014)) \
13            .build()
14
15        bob = Member.Builder("Bob", "[email protected]") \
16            .with_summary("Product Manager at Stripe.") \
17            .add_experience(Experience("Product Manager", "Stripe", date(2020, 2, 1), None)) \
18            .add_education(Education("MIT", "B.S. in Business Analytics", 2015, 2019)) \
19            .build()
20
21        charlie = Member.Builder("Charlie", "[email protected]").build()
22
23        system.register_member(alice)
24        system.register_member(bob)
25        system.register_member(charlie)
26
27        alice.display_profile()
28
29        # 2. Connection Management
30        print("\n--- 2. Connection Management ---")
31        # Alice sends requests to Bob and Charlie
32        request_id1 = system.send_connection_request(alice, bob)
33        request_id2 = system.send_connection_request(alice, charlie)
34
35        bob.view_notifications()  # Bob sees Alice's request
36
37        print("\nBob accepts Alice's request.")
38        system.accept_connection_request(request_id1)
39        print("Alice and Bob are now connected.")
40
41        # 3. Posting and News Feed
42        print("\n--- 3. Posting & News Feed ---")
43        bob.display_profile()  # Bob has 1 connection
44        system.create_post(bob.get_id(), "Excited to share we've launched our new feature! #productmanagement")
45
46        # Alice views her news feed. She should see Bob's post.
47        system.view_news_feed(alice.get_id())
48
49        # Charlie views his feed. It should be empty as he is not connected to anyone.
50        system.view_news_feed(charlie.get_id())
51
52        # 4. Interacting with a Post (Observer Pattern in action)
53        print("\n--- 4. Post Interaction & Notifications ---")
54        bobs_post = system.get_latest_post_by_member(bob.get_id())
55        if bobs_post:
56            bobs_post.add_like(alice)
57            bobs_post.add_comment(alice, "This looks amazing! Great work!")
58
59        # Bob checks his notifications. He should see a like and a comment from Alice.
60        bob.view_notifications()
61
62        # 5. Searching for Members
63        print("\n--- 5. Member Search ---")
64        search_results = system.search_member_by_name("ali")
65        print("Search results for 'ali':")
66        for member in search_results:
67            print(f" - {member.get_name()}")
68
69if __name__ == "__main__":
70    LinkedInDemo.main()

5. Run and Test

Files22
entities
enums
member
observers
services
strategies
linkedin_demo.py
main
linkedin_system.py
linkedin_demo.pymain
Output

6. Quiz

Design LinkedIn - Quiz

1 / 21
Multiple Choice

Which entity is primarily used to capture a member’s professional history in a LinkedIn-like system?

How helpful was this article?

Comments


0/2000

No comments yet. Be the first to comment!

Copilot extension content script